package nus.font;

import static java.lang.Math.max;
import static java.lang.Math.min;
import static nus.font.EAttribute.EBACKGROUND;
import static nus.font.EAttribute.EBASELINE_TRANSFORM;
import static nus.font.EAttribute.EBIDI_EMBEDDING;
import static nus.font.EAttribute.ECHAR_REPLACEMENT;
import static nus.font.EAttribute.EFAMILY;
import static nus.font.EAttribute.EFONT;
import static nus.font.EAttribute.EFOREGROUND;
import static nus.font.EAttribute.EINPUT_METHOD_HIGHLIGHT;
import static nus.font.EAttribute.EINPUT_METHOD_UNDERLINE;
import static nus.font.EAttribute.EJUSTIFICATION;
import static nus.font.EAttribute.EKERNING;
import static nus.font.EAttribute.ELIGATURES;
import static nus.font.EAttribute.ENUMERIC_SHAPING;
import static nus.font.EAttribute.EPOSTURE;
import static nus.font.EAttribute.ERUN_DIRECTION;
import static nus.font.EAttribute.ESIZE;
import static nus.font.EAttribute.ESTRIKETHROUGH;
import static nus.font.EAttribute.ESUPERSCRIPT;
import static nus.font.EAttribute.ESWAP_COLORS;
import static nus.font.EAttribute.ETRACKING;
import static nus.font.EAttribute.ETRANSFORM;
import static nus.font.EAttribute.EUNDERLINE;
import static nus.font.EAttribute.EWEIGHT;
import static nus.font.EAttribute.EWIDTH;

import java.awt.font.NumericShaper;
import java.awt.font.TextAttribute;
import java.io.Serializable;
import java.text.Annotation;
import java.text.AttributedCharacterIterator.Attribute;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import net.pbdavey.awt.Toolkit;
import and.awt.Font;
import and.awt.Paint;
import and.awt.font.GraphicAttribute;
import and.awt.font.TransformAttribute;
import and.awt.geom.AffineTransform;
import and.awt.geom.NoninvertibleTransformException;
import and.awt.geom.Point2D;
import and.awt.im.InputMethodHighlight;

public class AttributeValues {
	 private int defined;
	    private int nondefault;

	    private String family = "Default";
	    private float weight = 1f;
	    private float width = 1f;
	    private float posture; // 0f
	    private float size = 12f;
	    private float tracking; // 0f
	    private NumericShaper numericShaping; // null
	    private AffineTransform transform; // null == identity
	    private GraphicAttribute charReplacement; // null
	    private Paint foreground; // null
	    private Paint background; // null
	    private float justification = 1f;
	    private Object imHighlight; // null
	    // (can be either Attribute wrapping IMH, or IMH itself
	    private Font font; // here for completeness, don't actually use
	    private byte imUnderline = -1; // same default as underline
	    private byte superscript; // 0
	    private byte underline = -1; // arrgh, value for ON is 0
	    private byte runDirection = -2; // BIDI.DIRECTION_DEFAULT_LEFT_TO_RIGHT
	    private byte bidiEmbedding; // 0
	    private byte kerning; // 0
	    private byte ligatures; // 0
	    private boolean strikethrough; // false
	    private boolean swapColors; // false

	    private AffineTransform baselineTransform; // derived from transform
	    private AffineTransform charTransform; // derived from transform

	    private static final AttributeValues DEFAULT = new AttributeValues();

	    // type-specific API
	    public String getFamily() { return family; }
	    public void setFamily(String f) { this.family = f; update(EFAMILY); }

	    public float getWeight() { return weight; }
	    public void setWeight(float f) { this.weight = f; update(EWEIGHT); }

	    public float getWidth() { return width; }
	    public void setWidth(float f) { this.width = f; update(EWIDTH); }

	    public float getPosture() { return posture; }
	    public void setPosture(float f) { this.posture = f; update(EPOSTURE); }

	    public float getSize() { return size; }
	    public void setSize(float f) { this.size = f; update(ESIZE); }

	    public AffineTransform getTransform() { return transform; }
	    public void setTransform(AffineTransform f) {
	        this.transform = (f == null || f.isIdentity())
	            ? DEFAULT.transform
	            : new AffineTransform(f);
	        updateDerivedTransforms();
	        update(ETRANSFORM);
	    }
	    public void setTransform(TransformAttribute f) {
	        this.transform = (f == null || f.isIdentity())
	            ? DEFAULT.transform
	            : f.getTransform();
	        updateDerivedTransforms();
	        update(ETRANSFORM);
	    }

	    public int getSuperscript() { return superscript; }
	    public void setSuperscript(int f) {
	      this.superscript = (byte)f; update(ESUPERSCRIPT); }

	    public Font getFont() { return font; }
	    public void setFont(Font f) { this.font = f; update(EFONT); }

	    public GraphicAttribute getCharReplacement() { return charReplacement; }
	    public void setCharReplacement(GraphicAttribute f) {
	      this.charReplacement = f; update(ECHAR_REPLACEMENT); }

	    public Paint getForeground() { return foreground; }
	    public void setForeground(Paint f) {
	      this.foreground = f; update(EFOREGROUND); }

	    public Paint getBackground() { return background; }
	    public void setBackground(Paint f) {
	      this.background = f; update(EBACKGROUND); }

	    public int getUnderline() { return underline; }
	    public void setUnderline(int f) {
	      this.underline = (byte)f; update(EUNDERLINE); }

	    public boolean getStrikethrough() { return strikethrough; }
	    public void setStrikethrough(boolean f) {
	      this.strikethrough = f; update(ESTRIKETHROUGH); }

	    public int getRunDirection() { return runDirection; }
	    public void setRunDirection(int f) {
	      this.runDirection = (byte)f; update(ERUN_DIRECTION); }

	    public int getBidiEmbedding() { return bidiEmbedding; }
	    public void setBidiEmbedding(int f) {
	      this.bidiEmbedding = (byte)f; update(EBIDI_EMBEDDING); }

	    public float getJustification() { return justification; }
	    public void setJustification(float f) {
	      this.justification = f; update(EJUSTIFICATION); }

	    public Object getInputMethodHighlight() { return imHighlight; }
	    public void setInputMethodHighlight(Annotation f) {
	      this.imHighlight = f; update(EINPUT_METHOD_HIGHLIGHT); }
	    public void setInputMethodHighlight(InputMethodHighlight f) {
	      this.imHighlight = f; update(EINPUT_METHOD_HIGHLIGHT); }

	    public int getInputMethodUnderline() { return imUnderline; }
	    public void setInputMethodUnderline(int f) {
	      this.imUnderline = (byte)f; update(EINPUT_METHOD_UNDERLINE); }

	    public boolean getSwapColors() { return swapColors; }
	    public void setSwapColors(boolean f) {
	      this.swapColors = f; update(ESWAP_COLORS); }

	    public NumericShaper getNumericShaping() { return numericShaping; }
	    public void setNumericShaping(NumericShaper f) {
	      this.numericShaping = f; update(ENUMERIC_SHAPING); }

	    public int getKerning() { return kerning; }
	    public void setKerning(int f) {
	      this.kerning = (byte)f; update(EKERNING); }

	    public float getTracking() { return tracking; }
	    public void setTracking(float f) {
	      this.tracking = (byte)f; update(ETRACKING); }

	    public int getLigatures() { return ligatures; }
	    public void setLigatures(int f) {
	      this.ligatures = (byte)f; update(ELIGATURES); }


	    public AffineTransform getBaselineTransform() { return baselineTransform; }
	    public AffineTransform getCharTransform() { return charTransform; }

	    // mask api

	    public static int getMask(EAttribute att) {
	        return att.mask;
	    }

	    public static int getMask(EAttribute ... atts) {
	        int mask = 0;
	        for (EAttribute a: atts) {
	            mask |= a.mask;
	        }
	        return mask;
	    }

	    public static final int MASK_ALL =
	        getMask(EAttribute.class.getEnumConstants());

	    public void unsetDefault() {
	        defined &= nondefault;
	    }

	    public void defineAll(int mask) {
	        defined |= mask;
	        if ((defined & EBASELINE_TRANSFORM.mask) != 0) {
	            throw new InternalError("can't define derived attribute");
	        }
	    }

	    public boolean allDefined(int mask) {
	        return (defined & mask) == mask;
	    }

	    public boolean anyDefined(int mask) {
	        return (defined & mask) != 0;
	    }

	    public boolean anyNonDefault(int mask) {
	        return (nondefault & mask) != 0;
	    }

	    // generic EAttribute API

	    public boolean isDefined(EAttribute a) {
	        return (defined & a.mask) != 0;
	    }

	    public boolean isNonDefault(EAttribute a) {
	        return (nondefault & a.mask) != 0;
	    }

	    public void setDefault(EAttribute a) {
	        if (a.att == null) {
	            throw new InternalError("can't set default derived attribute: " + a);
	        }
	        i_set(a, DEFAULT);
	        defined |= a.mask;
	        nondefault &= ~a.mask;
	    }

	    public void unset(EAttribute a) {
	        if (a.att == null) {
	            throw new InternalError("can't unset derived attribute: " + a);
	        }
	        i_set(a, DEFAULT);
	        defined &= ~a.mask;
	        nondefault &= ~a.mask;
	    }

	    public void set(EAttribute a, AttributeValues src) {
	        if (a.att == null) {
	            throw new InternalError("can't set derived attribute: " + a);
	        }
	        if (src == null || src == DEFAULT) {
	            setDefault(a);
	        } else {
	            if ((src.defined & a.mask) != 0) {
	                i_set(a, src);
	                update(a);
	            }
	        }
	    }

	    public void set(EAttribute a, Object o) {
	        if (a.att == null) {
	            throw new InternalError("can't set derived attribute: " + a);
	        }
	        if (o != null) {
	            try {
	                i_set(a, o);
	                update(a);
	                return;
	            } catch (Exception e) {
	            }
	        }
	        setDefault(a);
	    }

	    public Object get(EAttribute a) {
	        if (a.att == null) {
	            throw new InternalError("can't get derived attribute: " + a);
	        }
	        if ((nondefault & a.mask) != 0) {
	            return i_get(a);
	        }
	        return null;
	    }

	    // merging

	    public AttributeValues merge(Map<? extends Attribute, ?>map) {
	        return merge(map, MASK_ALL);
	    }

	    public AttributeValues merge(Map<? extends Attribute, ?>map,
	                                 int mask) {
	        if (map instanceof AttributeMap &&
	            ((AttributeMap) map).getValues() != null) {
	            merge(((AttributeMap)map).getValues(), mask);
	        } else if (map != null && !map.isEmpty()) {
	            for (Map.Entry<? extends Attribute, ?> e: map.entrySet()) {
	                EAttribute ea = EAttribute.forAttribute(e.getKey());
	                if (ea!= null && (mask & ea.mask) != 0) {
	                    set(ea, e.getValue());
	                }
	            }
	        }
	        return this;
	    }

	    public AttributeValues merge(AttributeValues src) {
	        return merge(src, MASK_ALL);
	    }

	    public AttributeValues merge(AttributeValues src, int mask) {
	        int m = mask & src.defined;
	        for (EAttribute ea: EAttribute.atts) {
	            if (m == 0) {
	                break;
	            }
	            if ((m & ea.mask) != 0) {
	                m &= ~ea.mask;
	                i_set(ea, src);
	                update(ea);
	            }
	        }
	        return this;
	    }

	    // creation API

	    public static AttributeValues fromMap(Map<? extends Attribute, ?> map) {
	        return fromMap(map, MASK_ALL);
	    }

	    public static AttributeValues fromMap(Map<? extends Attribute, ?> map,
	                                          int mask) {
	        return new AttributeValues().merge(map, mask);
	    }

	    public Map<TextAttribute, Object> toMap(Map<TextAttribute, Object> fill) {
	        if (fill == null) {
	            fill = new HashMap<TextAttribute, Object>();
	        }

	        for (int m = defined, i = 0; m != 0; ++i) {
	            EAttribute ea = EAttribute.atts[i];
	            if ((m & ea.mask) != 0) {
	                m &= ~ea.mask;
	                fill.put(ea.att, get(ea));
	            }
	        }

	        return fill;
	    }

	    // key must be serializable, so use String, not Object
	    private static final String DEFINED_KEY =
	        "sun.font.attributevalues.defined_key";

	    public static boolean is16Hashtable(Hashtable<Object, Object> ht) {
	        return ht.containsKey(DEFINED_KEY);
	    }

	    public static AttributeValues
	    fromSerializableHashtable(Hashtable<Object, Object> ht)
	    {
	        AttributeValues result = new AttributeValues();
	        if (ht != null && !ht.isEmpty()) {
	            for (Map.Entry<Object, Object> e: ht.entrySet()) {
	                Object key = e.getKey();
	                Object val = e.getValue();
	                if (key.equals(DEFINED_KEY)) {
	                    result.defineAll(((Integer)val).intValue());
	                } else {
	                    try {
	                        EAttribute ea =
	                            EAttribute.forAttribute((Attribute)key);
	                        if (ea != null) {
	                            result.set(ea, val);
	                        }
	                    }
	                    catch (ClassCastException ex) {
	                    }
	                }
	            }
	        }
	        return result;
	    }

	    public Hashtable<Object, Object> toSerializableHashtable() {
	        Hashtable ht = new Hashtable();
	        int hashkey = defined;
	        for (int m = defined, i = 0; m != 0; ++i) {
	            EAttribute ea = EAttribute.atts[i];
	            if ((m & ea.mask) != 0) {
	                m &= ~ea.mask;
	                Object o = get(ea);
	                if (o == null) {
	                    // hashkey will handle it
	                } else if (o instanceof Serializable) { // check all...
	                    ht.put(ea.att, o);
	                } else {
	                    hashkey &= ~ea.mask;
	                }
	            }
	        }
	        ht.put(DEFINED_KEY, Integer.valueOf(hashkey));

	        return ht;
	    }

	    // boilerplate
	    public int hashCode() {
	        return defined << 8 ^ nondefault;
	    }

	    public boolean equals(Object rhs) {
	        try {
	            return equals((AttributeValues)rhs);
	        }
	        catch (ClassCastException e) {
	        }
	        return false;
	    }

	    public boolean equals(AttributeValues rhs) {
	        // test in order of most likely to differ and easiest to compare
	        // also assumes we're generally calling this only if family,
	        // size, weight, posture are the same

	        if (rhs == null) return false;
	        if (rhs == this) return true;

	        return defined == rhs.defined
	            && nondefault == rhs.nondefault
	            && underline == rhs.underline
	            && strikethrough == rhs.strikethrough
	            && superscript == rhs.superscript
	            && width == rhs.width
	            && kerning == rhs.kerning
	            && tracking == rhs.tracking
	            && ligatures == rhs.ligatures
	            && runDirection == rhs.runDirection
	            && bidiEmbedding == rhs.bidiEmbedding
	            && swapColors == rhs.swapColors
	            && equals(transform, rhs.transform)
	            && equals(foreground, rhs.foreground)
	            && equals(background, rhs.background)
	            && equals(numericShaping, rhs.numericShaping)
	            && equals(justification, rhs.justification)
	            && equals(charReplacement, rhs.charReplacement)
	            && size == rhs.size
	            && weight == rhs.weight
	            && posture == rhs.posture
	            && equals(family, rhs.family)
	            && equals(font, rhs.font)
	            && imUnderline == rhs.imUnderline
	            && equals(imHighlight, rhs.imHighlight);
	    }

	    public AttributeValues clone() {
	        try {
	            AttributeValues result = (AttributeValues)super.clone();
	            if (transform != null) { // AffineTransform is mutable
	                result.transform = new AffineTransform(transform);
	                result.updateDerivedTransforms();
	            }
	            // if transform is null, derived transforms are null
	            // so there's nothing to do
	            return result;
	        }
	        catch (CloneNotSupportedException e) {
	            // never happens
	            return null;
	        }
	    }

	    public String toString() {
	        StringBuilder b = new StringBuilder();
	        b.append('{');
	        for (int m = defined, i = 0; m != 0; ++i) {
	            EAttribute ea = EAttribute.atts[i];
	            if ((m & ea.mask) != 0) {
	                m &= ~ea.mask;
	                if (b.length() > 1) {
	                    b.append(", ");
	                }
	                b.append(ea);
	                b.append('=');
	                switch (ea) {
	                case EFAMILY: b.append('"');
	                  b.append(family);
	                  b.append('"'); break;
	                case EWEIGHT: b.append(weight); break;
	                case EWIDTH: b.append(width); break;
	                case EPOSTURE: b.append(posture); break;
	                case ESIZE: b.append(size); break;
	                case ETRANSFORM: b.append(transform); break;
	                case ESUPERSCRIPT: b.append(superscript); break;
	                case EFONT: b.append(font); break;
	                case ECHAR_REPLACEMENT: b.append(charReplacement); break;
	                case EFOREGROUND: b.append(foreground); break;
	                case EBACKGROUND: b.append(background); break;
	                case EUNDERLINE: b.append(underline); break;
	                case ESTRIKETHROUGH: b.append(strikethrough); break;
	                case ERUN_DIRECTION: b.append(runDirection); break;
	                case EBIDI_EMBEDDING: b.append(bidiEmbedding); break;
	                case EJUSTIFICATION: b.append(justification); break;
	                case EINPUT_METHOD_HIGHLIGHT: b.append(imHighlight); break;
	                case EINPUT_METHOD_UNDERLINE: b.append(imUnderline); break;
	                case ESWAP_COLORS: b.append(swapColors); break;
	                case ENUMERIC_SHAPING: b.append(numericShaping); break;
	                case EKERNING: b.append(kerning); break;
	                case ELIGATURES: b.append(ligatures); break;
	                case ETRACKING: b.append(tracking); break;
	                default: throw new InternalError();
	                }
	                if ((nondefault & ea.mask) == 0) {
	                    b.append('*');
	                }
	            }
	        }
	        b.append("[btx=" + baselineTransform + ", ctx=" + charTransform + "]");
	        b.append('}');
	        return b.toString();
	    }

	    // internal utilities

	    private static boolean equals(Object lhs, Object rhs) {
	        return lhs == null ? rhs == null : lhs.equals(rhs);
	    }

	    private void update(EAttribute a) {
	        defined |= a.mask;
	        if (i_validate(a)) {
	            if (i_equals(a, DEFAULT)) {
	                nondefault &= ~a.mask;
	            } else {
	                nondefault |= a.mask;
	            }
	        } else {
	            setDefault(a);
	        }
	    }

	    // dispatch

	    private void i_set(EAttribute a, AttributeValues src) {
	        switch (a) {
	        case EFAMILY: family = src.family; break;
	        case EWEIGHT: weight = src.weight; break;
	        case EWIDTH: width = src.width; break;
	        case EPOSTURE: posture = src.posture; break;
	        case ESIZE: size = src.size; break;
	        case ETRANSFORM: transform = src.transform; updateDerivedTransforms(); break;
	        case ESUPERSCRIPT: superscript = src.superscript; break;
	        case EFONT: font = src.font; break;
	        case ECHAR_REPLACEMENT: charReplacement = src.charReplacement; break;
	        case EFOREGROUND: foreground = src.foreground; break;
	        case EBACKGROUND: background = src.background; break;
	        case EUNDERLINE: underline = src.underline; break;
	        case ESTRIKETHROUGH: strikethrough = src.strikethrough; break;
	        case ERUN_DIRECTION: runDirection = src.runDirection; break;
	        case EBIDI_EMBEDDING: bidiEmbedding = src.bidiEmbedding; break;
	        case EJUSTIFICATION: justification = src.justification; break;
	        case EINPUT_METHOD_HIGHLIGHT: imHighlight = src.imHighlight; break;
	        case EINPUT_METHOD_UNDERLINE: imUnderline = src.imUnderline; break;
	        case ESWAP_COLORS: swapColors = src.swapColors; break;
	        case ENUMERIC_SHAPING: numericShaping = src.numericShaping; break;
	        case EKERNING: kerning = src.kerning; break;
	        case ELIGATURES: ligatures = src.ligatures; break;
	        case ETRACKING: tracking = src.tracking; break;
	        default: throw new InternalError();
	        }
	    }

	    private boolean i_equals(EAttribute a, AttributeValues src) {
	        switch (a) {
	        case EFAMILY: return equals(family, src.family);
	        case EWEIGHT: return weight == src.weight;
	        case EWIDTH: return width == src.width;
	        case EPOSTURE: return posture == src.posture;
	        case ESIZE: return size == src.size;
	        case ETRANSFORM: return equals(transform, src.transform);
	        case ESUPERSCRIPT: return superscript == src.superscript;
	        case EFONT: return equals(font, src.font);
	        case ECHAR_REPLACEMENT: return equals(charReplacement, src.charReplacement);
	        case EFOREGROUND: return equals(foreground, src.foreground);
	        case EBACKGROUND: return equals(background, src.background);
	        case EUNDERLINE: return underline == src.underline;
	        case ESTRIKETHROUGH: return strikethrough == src.strikethrough;
	        case ERUN_DIRECTION: return runDirection == src.runDirection;
	        case EBIDI_EMBEDDING: return bidiEmbedding == src.bidiEmbedding;
	        case EJUSTIFICATION: return justification == src.justification;
	        case EINPUT_METHOD_HIGHLIGHT: return equals(imHighlight, src.imHighlight);
	        case EINPUT_METHOD_UNDERLINE: return imUnderline == src.imUnderline;
	        case ESWAP_COLORS: return swapColors == src.swapColors;
	        case ENUMERIC_SHAPING: return equals(numericShaping, src.numericShaping);
	        case EKERNING: return kerning == src.kerning;
	        case ELIGATURES: return ligatures == src.ligatures;
	        case ETRACKING: return tracking == src.tracking;
	        default: throw new InternalError();
	        }
	    }

	    private void i_set(EAttribute a, Object o) {
	        switch (a) {
	        case EFAMILY: family = ((String)o).trim(); break;
	        case EWEIGHT: weight = ((Number)o).floatValue(); break;
	        case EWIDTH: width = ((Number)o).floatValue(); break;
	        case EPOSTURE: posture = ((Number)o).floatValue(); break;
	        case ESIZE: size = ((Number)o).floatValue(); break;
	        case ETRANSFORM: {
	            if (o instanceof TransformAttribute) {
	                TransformAttribute ta = (TransformAttribute)o;
	                if (ta.isIdentity()) {
	                    transform = null;
	                } else {
	                    transform = ta.getTransform();
	                }
	            } else {
	                transform = new AffineTransform((AffineTransform)o);
	            }
	            updateDerivedTransforms();
	        } break;
	        case ESUPERSCRIPT: superscript = (byte)((Integer)o).intValue(); break;
	        case EFONT: font = (Font)o; break;
	        case ECHAR_REPLACEMENT: charReplacement = (GraphicAttribute)o; break;
	        case EFOREGROUND: foreground = (Paint)o; break;
	        case EBACKGROUND: background = (Paint)o; break;
	        case EUNDERLINE: underline = (byte)((Integer)o).intValue(); break;
	        case ESTRIKETHROUGH: strikethrough = ((Boolean)o).booleanValue(); break;
	        case ERUN_DIRECTION: {
	            if (o instanceof Boolean) {
	                runDirection = (byte)(TextAttribute.RUN_DIRECTION_LTR.equals(o) ? 0 : 1);
	            } else {
	                runDirection = (byte)((Integer)o).intValue();
	            }
	        } break;
	        case EBIDI_EMBEDDING: bidiEmbedding = (byte)((Integer)o).intValue(); break;
	        case EJUSTIFICATION: justification = ((Number)o).floatValue(); break;
	        case EINPUT_METHOD_HIGHLIGHT: {
	            if (o instanceof Annotation) {
	                Annotation at = (Annotation)o;
	                imHighlight = (InputMethodHighlight)at.getValue();
	            } else {
	                imHighlight = (InputMethodHighlight)o;
	            }
	        } break;
	        case EINPUT_METHOD_UNDERLINE: imUnderline = (byte)((Integer)o).intValue();
	          break;
	        case ESWAP_COLORS: swapColors = ((Boolean)o).booleanValue(); break;
	        case ENUMERIC_SHAPING: numericShaping = (NumericShaper)o; break;
	        case EKERNING: kerning = (byte)((Integer)o).intValue(); break;
	        case ELIGATURES: ligatures = (byte)((Integer)o).intValue(); break;
	        case ETRACKING: tracking = ((Number)o).floatValue(); break;
	        default: throw new InternalError();
	        }
	    }

	    private Object i_get(EAttribute a) {
	        switch (a) {
	        case EFAMILY: return family;
	        case EWEIGHT: return Float.valueOf(weight);
	        case EWIDTH: return Float.valueOf(width);
	        case EPOSTURE: return Float.valueOf(posture);
	        case ESIZE: return Float.valueOf(size);
	        case ETRANSFORM:
	            return transform == null
	                ? TransformAttribute.IDENTITY
	                : new TransformAttribute(transform);
	        case ESUPERSCRIPT: return Integer.valueOf(superscript);
	        case EFONT: return font;
	        case ECHAR_REPLACEMENT: return charReplacement;
	        case EFOREGROUND: return foreground;
	        case EBACKGROUND: return background;
	        case EUNDERLINE: return Integer.valueOf(underline);
	        case ESTRIKETHROUGH: return Boolean.valueOf(strikethrough);
	        case ERUN_DIRECTION: {
	            switch (runDirection) {
	                // todo: figure out a way to indicate this value
	                // case -1: return Integer.valueOf(runDirection);
	            case 0: return TextAttribute.RUN_DIRECTION_LTR;
	            case 1: return TextAttribute.RUN_DIRECTION_RTL;
	            default: return null;
	            }
	        } // not reachable
	        case EBIDI_EMBEDDING: return Integer.valueOf(bidiEmbedding);
	        case EJUSTIFICATION: return Float.valueOf(justification);
	        case EINPUT_METHOD_HIGHLIGHT: return imHighlight;
	        case EINPUT_METHOD_UNDERLINE: return Integer.valueOf(imUnderline);
	        case ESWAP_COLORS: return Boolean.valueOf(swapColors);
	        case ENUMERIC_SHAPING: return numericShaping;
	        case EKERNING: return Integer.valueOf(kerning);
	        case ELIGATURES: return Integer.valueOf(ligatures);
	        case ETRACKING: return Float.valueOf(tracking);
	        default: throw new InternalError();
	        }
	    }

	    private boolean i_validate(EAttribute a) {
	        switch (a) {
	        case EFAMILY: if (family == null || family.length() == 0)
	          family = DEFAULT.family; return true;
	        case EWEIGHT: return weight > 0 && weight < 10;
	        case EWIDTH: return width >= .5f && width < 10;
	        case EPOSTURE: return posture >= -1 && posture <= 1;
	        case ESIZE: return size >= 0;
	        case ETRANSFORM: if (transform != null && transform.isIdentity())
	            transform = DEFAULT.transform; return true;
	        case ESUPERSCRIPT: return superscript >= -7 && superscript <= 7;
	        case EFONT: return true;
	        case ECHAR_REPLACEMENT: return true;
	        case EFOREGROUND: return true;
	        case EBACKGROUND: return true;
	        case EUNDERLINE: return underline >= -1 && underline < 6;
	        case ESTRIKETHROUGH: return true;
	        case ERUN_DIRECTION: return runDirection >= -2 && runDirection <= 1;
	        case EBIDI_EMBEDDING: return bidiEmbedding >= -61 && bidiEmbedding < 62;
	        case EJUSTIFICATION: justification = max(0, min (justification, 1));
	            return true;
	        case EINPUT_METHOD_HIGHLIGHT: return true;
	        case EINPUT_METHOD_UNDERLINE: return imUnderline >= -1 && imUnderline < 6;
	        case ESWAP_COLORS: return true;
	        case ENUMERIC_SHAPING: return true;
	        case EKERNING: return kerning >= 0 && kerning <= 1;
	        case ELIGATURES: return ligatures >= 0 && ligatures <= 1;
	        case ETRACKING: return tracking >= -1 && tracking <= 10;
	        default: throw new InternalError("unknown attribute: " + a);
	        }
	    }

	    // Until textlayout is fixed to use AttributeValues, we'll end up
	    // creating a map from the values for it.  This is a compromise between
	    // creating the whole map and just checking a particular value.
	    // Plan to remove these.
	    public static float getJustification(Map<?, ?> map) {
	        if (map != null) {
	            if (map instanceof AttributeMap &&
	                ((AttributeMap) map).getValues() != null) {
	                return ((AttributeMap)map).getValues().justification;
	            }
	            Object obj = map.get(TextAttribute.JUSTIFICATION);
	            if (obj != null && obj instanceof Number) {
	                return max(0, min(1, ((Number)obj).floatValue()));
	            }
	        }
	        return DEFAULT.justification;
	    }

	    public static NumericShaper getNumericShaping(Map<?, ?> map) {
	        if (map != null) {
	            if (map instanceof AttributeMap &&
	                ((AttributeMap) map).getValues() != null) {
	                return ((AttributeMap)map).getValues().numericShaping;
	            }
	            Object obj = map.get(TextAttribute.NUMERIC_SHAPING);
	            if (obj != null && obj instanceof NumericShaper) {
	                return (NumericShaper)obj;
	            }
	        }
	        return DEFAULT.numericShaping;
	    }

	    /**
	     * If this has an imHighlight, create copy of this with those attributes
	     * applied to it.  Otherwise return this unchanged.
	     */
	    public AttributeValues applyIMHighlight() {
	        if (imHighlight != null) {
	            InputMethodHighlight hl = null;
	            if (imHighlight instanceof InputMethodHighlight) {
	                hl = (InputMethodHighlight)imHighlight;
	            } else {
	                hl = (InputMethodHighlight)((Annotation)imHighlight).getValue();
	            }

	            Map imStyles = hl.getStyle();
	            if (imStyles == null) {
	                Toolkit tk = Toolkit.getDefaultToolkit();
	                imStyles = tk.mapInputMethodHighlight(hl);
	            }

	            if (imStyles != null) {
	                return clone().merge(imStyles);
	            }
	        }

	        return this;
	    }

	    public static AffineTransform getBaselineTransform(Map<?, ?> map) {
	        if (map != null) {
	            AttributeValues av = null;
	            if (map instanceof AttributeMap &&
	                ((AttributeMap) map).getValues() != null) {
	                av = ((AttributeMap)map).getValues();
	            } else if (map.get(TextAttribute.TRANSFORM) != null) {
	                av = AttributeValues.fromMap((Map<Attribute, ?>)map); // yuck
	            }
	            if (av != null) {
	                return av.baselineTransform;
	            }
	        }
	        return null;
	    }

	    public static AffineTransform getCharTransform(Map<?, ?> map) {
	        if (map != null) {
	            AttributeValues av = null;
	            if (map instanceof AttributeMap &&
	                ((AttributeMap) map).getValues() != null) {
	                av = ((AttributeMap)map).getValues();
	            } else if (map.get(TextAttribute.TRANSFORM) != null) {
	                av = AttributeValues.fromMap((Map<Attribute, ?>)map); // yuck
	            }
	            if (av != null) {
	                return av.charTransform;
	            }
	        }
	        return null;
	    }

	    public void updateDerivedTransforms() {
	        // this also updates the mask for the baseline transform
	        if (transform == null) {
	            baselineTransform = null;
	            charTransform = null;
	        } else {
	            charTransform = new AffineTransform(transform);
	            baselineTransform = extractXRotation(charTransform, true);

	            if (charTransform.isIdentity()) {
	              charTransform = null;
	            }

	            if (baselineTransform.isIdentity()) {
	              baselineTransform = null;
	            }
	        }

	        if (baselineTransform == null) {
	            nondefault &= ~EBASELINE_TRANSFORM.mask;
	        } else {
	            nondefault |= EBASELINE_TRANSFORM.mask;
	        }
	    }

	    public static AffineTransform extractXRotation(AffineTransform tx,
	                                                   boolean andTranslation) {
	        return extractRotation(new Point2D.Double(1, 0), tx, andTranslation);
	    }

	    public static AffineTransform extractYRotation(AffineTransform tx,
	                                                   boolean andTranslation) {
	        return extractRotation(new Point2D.Double(0, 1), tx, andTranslation);
	    }

	    private static AffineTransform extractRotation(Point2D.Double pt,
	        AffineTransform tx, boolean andTranslation) {

	        tx.deltaTransform(pt, pt);
	        AffineTransform rtx = AffineTransform.getRotateInstance(pt.x, pt.y);

	        try {
	            AffineTransform rtxi = rtx.createInverse();
	            double dx = tx.getTranslateX();
	            double dy = tx.getTranslateY();
	            tx.preConcatenate(rtxi);
	            if (andTranslation) {
	                if (dx != 0 || dy != 0) {
	                    tx.setTransform(tx.getScaleX(), tx.getShearY(),
	                                    tx.getShearX(), tx.getScaleY(), 0, 0);
	                    rtx.setTransform(rtx.getScaleX(), rtx.getShearY(),
	                                     rtx.getShearX(), rtx.getScaleY(), dx, dy);
	                }
	            }
	        }
	        catch (NoninvertibleTransformException e) {
	            return null;
	        }
	        return rtx;
	    }

}
